Two-Way Main-Renderer IPC
See the Electron-Documentation for a more indepth explanation of the reasons for, and how the IPC works between the Main process, the Renderer process, and why the preload step is required.
Unlike typical Fable.Remoting, the entire stack is compiled to JavaScript, so there is no
requirement to define your API types in a Shared project. You can choose to do this or not.
Example
The Two-Way Main-Renderer IPC channel allows the client/renderer process to
send a message to the main process, and to then receive a response asynchronously.
This is required due to the Renderer processes not having access to the Node.js API, nor
the full Electron API.
A full example is provided below:
- Shared.fs
- main.fs
- preload.fs
- renderer.fs
open Fable.Core.JS // Promise type
type ExampleRouting = {
SayHelloWorld: string -> Promise<Result<string, unit>>
}
open Fable.Electron
open Fable.Core
open Fable.Core.JsInterop
open Fable.Core.JS
open Node.Api
open Node.Base
open Fable.Electron.Main
open Fable.Electron.Remoting.Main
if SquirrelStartup.started then
app.quit()
let createWindow() =
let mainWindowOptions =
BrowserWindowConstructorOptions(
width = 800,
height = 600,
webPreferences = WebPreferences(
preload = path.join(__dirname, "preload.js")
)
)
let mainWindow = BrowserWindow(mainWindowOptions)
if isNullOrUndefined MAIN_WINDOW_VITE_DEV_SERVER_URL
then mainWindow.loadFile(path.join(__dirname, $"../renderer/{MAIN_WINDOW_VITE_NAME}/index.html"))
else mainWindow.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL)
|> ignore
mainWindow.webContents.openDevTools(Enums.WebContents.OpenDevTools.Options.Mode.Right)
app.whenReady().``then``(fun () ->
let api: Shared.ExampleRouting = {
SayHelloWorld = fun text -> promise {
if text = "hello" then
return Ok <| text + " world!"
else
return Error()
}
}
Remoting.init
|> Remoting.buildHandler(api)
createWindow()
app.onActivate(fun _ ->
if BrowserWindow.getAllWindows().Length = 0 then
createWindow()
)
)
|> ignore
app.onWindowAllClosed(fun () ->
if not Node.Api.process.platform.IsDarwin then
app.quit()
)
open Fable.Electron.Playground.Shared
open Fable.Electron.Remoting.Preload
Remoting.init
|> Remoting.buildTwoWayBridge<ExampleRouting>
open Fable.Core.JsInterop
open Fable.Electron.Remoting.Renderer
open Browser.Dom
importSideEffects "./index.css"
console.log "This message is being logged by 'renderer.js', included via VITE"
let api =
Remoting.init
|> Remoting.buildClient<Shared.ExampleRouting>
(api.SayHelloWorld "hello").``then``(function
| Ok v -> console.log v
| Error _ -> console.log "Didn't say hello back :(")
|> ignore
The API for Fable.Electron.Remoting is highly likely to change
according to user preference at the beginning.
IpcMainEvent
If you are familiar with Electron, or have checked the documentation, you'll notice that there is no inherent inclusion of the IpcMainEvent argument which is usually received as the first parameter for the Main process.
This is intentional, naturally, as including it destroys the type safety of our function signatures in F#, and renders us unable to use one API type as the source of truth.
Under the hood, we use an intermediary lambda which discards the first argument on the Receiver (Main process).
If you want to keep and use the IpcMainEvent, just include it as the FIRST parameter
of your function signature. Pass null for this value (or Unchecked.defaultof<_>, or undefined) on the renderer side.
type ExampleRouting = {
SayHelloWorld: IpcMainEvent -> string -> Promise<Result<string, unit>>
}
let api =
Remoting.init
|> Remoting.buildClient<Shared.ExampleRouting>
(api.SayHelloWorld JS.undefined "hello").``then``(function
| Ok v -> console.log v
| Error _ -> console.log "Didn't say hello back :(")
|> ignore
The preload script will ignore the first argument it receives from the Renderer
if it is of the type IpcMainEvent, and you will have the correct types on
the receivers (Main) end.
let api: Shared.ExampleRouting = {
SayHelloWorld = fun (event: IpcMainEvent) (text: string) -> promise {
if text = "hello" then
return Ok <| text + " world!"
else
return Error()
}
}